/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.iotdb.db.utils;

import org.apache.iotdb.commons.conf.CommonConfig;
import org.apache.iotdb.commons.conf.CommonDescriptor;
import org.apache.iotdb.commons.conf.IoTDBConstant;
import org.apache.iotdb.db.conf.IoTDBConfig;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.conf.directories.DirectoryManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;

// Notice : statistics in this class may not be accurate because of limited user authority.
public class OpenFileNumUtil {

    private static final Logger logger = LoggerFactory.getLogger(OpenFileNumUtil.class);
    private static final int PID_ERROR_CODE = -1;
    private static final int UNSUPPORTED_OS_ERROR_CODE = -2;
    private static final int UNKNOWN_STATISTICS_ERROR_CODE = -3;
    private static final String IOTDB_PROCESS_KEY_WORD = IoTDBConstant.GLOBAL_DB_NAME;
    private static final String LINUX_OS_NAME = "linux";
    private static final String MAC_OS_NAME = "mac";
    private static final String SEARCH_PID_LINUX = "ps -aux | grep -i %s | grep -v grep";
    private static final String SEARCH_PID_MAC = "ps aux | grep -i %s | grep -v grep";
    // command 'lsof -p' is available on most Linux distro except CentOS.
    private static final String SEARCH_OPEN_DATA_FILE_BY_PID = "lsof -p %d";

    private static IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
    private static CommonConfig commonConfig = CommonDescriptor.getInstance().getConfig();
    private static DirectoryManager directoryManager = DirectoryManager.getInstance();
    private static final String[] COMMAND_TEMPLATE = {"/bin/bash", "-c", ""};
    private static boolean isOutputValid = false;
    private int pid;

    /**
     * constructor, process key word is defined by IOTDB_PROCESS_KEY_WORD.
     */
    private OpenFileNumUtil() {
        pid = getIotdbPid();
    }

    /**
     * singleton instance.
     *
     * @return instance
     */
    public static OpenFileNumUtil getInstance() {
        return OpenFileNumUtilHolder.INSTANCE;
    }

    /**
     * check if the string is numeric.
     *
     * @param str string need to be checked
     * @return whether the string is a number
     */
    private static boolean isNumeric(String str) {
        if (str == null || "".equals(str)) {
            return false;
        } else {
            for (int i = str.length(); --i >= 0; ) {
                if (!Character.isDigit(str.charAt(i))) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * get IoTDB server process ID by executing command.
     *
     * @return pid of IoTDB server process
     */
    private static int getIotdbPid() {
        int iotdbPid = -1;
        Process pro1;
        Runtime r = Runtime.getRuntime();
        // System.getProperty("os.name") can detect which type of OS is using now.
        // this code can detect Windows, Mac, Unix and Solaris.
        String os = System.getProperty("os.name");
        String osName = os.toLowerCase();
        if (osName.startsWith(LINUX_OS_NAME) || osName.startsWith(MAC_OS_NAME)) {
            try {
                String command;
                if (osName.startsWith(LINUX_OS_NAME)) {
                    command = String.format(SEARCH_PID_LINUX, IOTDB_PROCESS_KEY_WORD);
                } else {
                    command = String.format(SEARCH_PID_MAC, IOTDB_PROCESS_KEY_WORD);
                }
                COMMAND_TEMPLATE[2] = command;
                pro1 = r.exec(COMMAND_TEMPLATE);
                BufferedReader in1 = new BufferedReader(new InputStreamReader(pro1.getInputStream()));
                String line;
                while ((line = in1.readLine()) != null) {
                    line = line.trim();
                    String[] temp = line.split("\\s+");
                    if (temp.length > 1 && isNumeric(temp[1])) {
                        iotdbPid = Integer.parseInt(temp[1]);
                        break;
                    }
                }
                in1.close();
                pro1.destroy();
            } catch (IOException e) {
                logger.error("Cannot get PID of IoTDB process because ", e);
            }
        } else {
            logger.warn("Unsupported OS {} for OpenFileNumUtil to get the PID of IoTDB.", os);
        }
        return iotdbPid;
    }

    /**
     * set pid.
     *
     * @param pid is the process ID of IoTDB service process
     */
    void setPid(int pid) {
        this.pid = pid;
    }

    /**
     * return statistic Map, whose key belongs to enum OpenFileNumStatistics: TOTAL_OPEN_FILE_NUM is
     * the current total open file number of IoTDB service process; SEQUENCE_FILE_OPEN_NUM is the
     * current open file number under data directory; DELTA_OPEN_FILE_NUM is the current open file
     * number of TsFile; UNSEQUENCE_FILE_OPEN_NUM is the current open file number of unsequence file;
     * WAL_OPEN_FILE_NUM is the current open file number of WAL file; METADATA_OPEN_FILE_NUM is the
     * current open file number of metadata; DIGEST_OPEN_FILE_NUM is the current open file number of
     * fileNodeDir; SOCKET_OPEN_FILE_NUM is the current open socket connection of IoTDB service
     * process.
     *
     * @param pid : IoTDB service pid
     * @return list : statistics list
     */
    private static EnumMap<OpenFileNumStatistics, Integer> getOpenFile(int pid) {
        EnumMap<OpenFileNumStatistics, Integer> resultMap = new EnumMap<>(OpenFileNumStatistics.class);
        // initialize resultMap
        for (OpenFileNumStatistics openFileNumStatistics : OpenFileNumStatistics.values()) {
            resultMap.put(openFileNumStatistics, 0);
        }
        Process pro;
        int lineCount = 0;
        Runtime r = Runtime.getRuntime();
        try {
            String command = String.format(SEARCH_OPEN_DATA_FILE_BY_PID, pid);
            COMMAND_TEMPLATE[2] = command;
            pro = r.exec(COMMAND_TEMPLATE);
            String line;

            try (BufferedReader in = new BufferedReader(new InputStreamReader(pro.getInputStream()))) {
                while ((line = in.readLine()) != null) {
                    lineCount++;
                    countOneFile(line, pid, resultMap);
                }
            }
            if (lineCount < OpenFileNumStatistics.values().length) {
                isOutputValid = false;
                for (OpenFileNumStatistics statistics : OpenFileNumStatistics.values()) {
                    resultMap.put(statistics, UNSUPPORTED_OS_ERROR_CODE);
                }
            } else {
                isOutputValid = true;
            }
            pro.destroy();
        } catch (Exception e) {
            logger.error("Cannot get open file number of IoTDB process because ", e);
        }
        return resultMap;
    }

    private static void countOneFile(
            String line, int pid, EnumMap<OpenFileNumStatistics, Integer> resultMap) {
        String[] temp = line.split("\\s+");
        if (!line.contains(Integer.toString(pid)) || temp.length <= 8) {
            return;
        }
        int oldValue = resultMap.get(OpenFileNumStatistics.TOTAL_OPEN_FILE_NUM);
        resultMap.put(OpenFileNumStatistics.TOTAL_OPEN_FILE_NUM, oldValue + 1);
        for (OpenFileNumStatistics openFileNumStatistics : OpenFileNumStatistics.values()) {
            if (openFileNumStatistics.path == null) {
                continue;
            }
            for (String path : openFileNumStatistics.path) {
                if (temp[8].contains(path)) {
                    oldValue = resultMap.get(openFileNumStatistics);
                    resultMap.put(openFileNumStatistics, oldValue + 1);
                }
            }
        }
        if (temp[7].contains("TCP") || temp[7].contains("UDP")) {
            oldValue = resultMap.get(OpenFileNumStatistics.SOCKET_OPEN_FILE_NUM);
            resultMap.put(OpenFileNumStatistics.SOCKET_OPEN_FILE_NUM, oldValue + 1);
        }
    }

    /**
     * Check if runtime OS is supported then return the result list. If pid is abnormal then all
     * statistics returns -1, if OS is not supported then all statistics returns -2
     *
     * @return map
     */
    private EnumMap<OpenFileNumStatistics, Integer> getStatisticMap() {
        EnumMap<OpenFileNumStatistics, Integer> resultMap = new EnumMap<>(OpenFileNumStatistics.class);
        String osName = System.getProperty("os.name").toLowerCase();
        // get runtime OS name, currently only support Linux and MacOS
        if (osName.startsWith(LINUX_OS_NAME) || osName.startsWith(MAC_OS_NAME)) {
            // if pid is normal, then get statistics
            if (pid > 0) {
                resultMap = getOpenFile(pid);
            } else {
                // pid is abnormal, give all statistics abnormal value -1
                for (OpenFileNumStatistics statistics : OpenFileNumStatistics.values()) {
                    resultMap.put(statistics, PID_ERROR_CODE);
                }
            }
        } else {
            // operation system not supported, give all statistics abnormal value -2
            for (OpenFileNumStatistics statistics : OpenFileNumStatistics.values()) {
                resultMap.put(statistics, UNSUPPORTED_OS_ERROR_CODE);
            }
        }
        return resultMap;
    }

    /**
     * get statistics.
     *
     * @param statistics get what statistics of open file number
     * @return open file number
     */
    public int get(OpenFileNumStatistics statistics) {
        EnumMap<OpenFileNumStatistics, Integer> statisticsMap = getStatisticMap();
        return statisticsMap.getOrDefault(statistics, UNKNOWN_STATISTICS_ERROR_CODE);
    }

    boolean isCommandValid() {
        return isOutputValid;
    }

    public enum OpenFileNumStatistics {
        TOTAL_OPEN_FILE_NUM(null),
        SEQUENCE_FILE_OPEN_NUM(directoryManager.getAllSequenceFileFolders()),
        UNSEQUENCE_FILE_OPEN_NUM(directoryManager.getAllUnSequenceFileFolders()),
        WAL_OPEN_FILE_NUM(Arrays.asList(commonConfig.getWalDirs())),
        DIGEST_OPEN_FILE_NUM(Collections.singletonList(config.getSystemDir())),
        SOCKET_OPEN_FILE_NUM(null);

        // path is a list of directory corresponding to the OpenFileNumStatistics enum element,
        // e.g. data/data/ for SEQUENCE_FILE_OPEN_NUM
        private List<String> path;

        OpenFileNumStatistics(List<String> path) {
            this.path = path;
        }

        public List<String> getPath() {
            return path;
        }
    }

    private static class OpenFileNumUtilHolder {

        private OpenFileNumUtilHolder() {
        }

        private static final OpenFileNumUtil INSTANCE = new OpenFileNumUtil();
    }
}
